package reliability.ui;

import reliability.model.GraphModel;
import reliability.model.GraphModelEvent;
import reliability.model.GraphModelListener;
import reliability.types.graph.*;

import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Map;

public class GraphPanel
        extends JPanel
        implements MouseListener, GraphModelListener, PopupMenuListener, ActionListener
{
    private static final int GRID_SIZE = 1;
    private static final int CELL_SIZE = 24;
    private static final int EXTERNAL_BORDER_OFFSET = 3;
    private static final int ROWS_COUNT = 40;
    private static final int COLS_COUNT = 40;
    private static final double EDGE_CLICK_DISTANCE = 15.0;
    private static final int EDGE_LABEL_OFFSET_Y = 8;
    private static final int EDGE_LABEL_OFFSET_X = 3;
    private static final int RESERVED_LINE_OFFSET = 4;

    private GraphModel model;
    NodeLabel [][] nodeLabelMatrix = new NodeLabel[ROWS_COUNT][COLS_COUNT];
    private GraphPanelPopup popup;
    private boolean preventMouseEventHandler = false;
    private Point lastPopupTriggeredPoint;
    private Point clickedCellIndex;
    private boolean mouseIsInside = true;

    GraphPanel(GraphModel model)
    {
        super();
        this.model = model;
        model.addListener(this);
        setFocusable(true);
        requestFocusInWindow();
        setPreferredSize(new Dimension(800, 800));

        setLayout(null);
        addMouseListener(this);
        popup = new GraphPanelPopup(this);
        popup.addPopupMenuListener(this);
        updateItems();
    }

    public void graphModelChanged(GraphModelEvent graphModelEvent)
    {

        removeAll();
        updateItems();
        repaint();
    }

    public void actionPerformed(ActionEvent actionEvent)
    {
        preventMouseEventHandler = false;
        String command = actionEvent.getActionCommand();
        switch (popup.getType())
        {
            case NODE_POPUP:
                NodeLabel hoveredLabel = findNodeLabelByPoint(lastPopupTriggeredPoint);
                if (hoveredLabel != null)
                {
                    Integer hoveredLabelKey = hoveredLabel.getKey();
                    if (command.equals(GraphPanelPopup.ADD_EDGE_COMMAND))
                    {
                        model.addEdgeFromSelectedNode(hoveredLabelKey);
                    }
                    else if (command.equals(GraphPanelPopup.DELETE_COMMAND))
                    {
                        model.deleteNode(hoveredLabelKey);
                    }
                }
                break;
            case EDGE_POPUP:
                Integer hoveredEdgeKey = findSelectedEdgeKey(lastPopupTriggeredPoint);
                if (command.equals(GraphPanelPopup.DELETE_COMMAND) && hoveredEdgeKey != null)
                {
                    model.deleteEdge(hoveredEdgeKey);
                }
                break;
            case EMPTY_POPUP:
                Point index = getIndexInGrid(lastPopupTriggeredPoint);
                addNodeToModel(index);
                break;
        }
    }

    public void mousePressed(MouseEvent mouseEvent)
    {
        requestFocusInWindow();
        if (mouseEvent.getButton() == MouseEvent.BUTTON1)
        {
            handleLeftMousePressedEvent(mouseEvent);
        }
        else if (mouseEvent.isPopupTrigger())
        {
            handlePopupTrigger(mouseEvent);
        }
    }

    private void handleLeftMousePressedEvent(MouseEvent mouseEvent)
    {
        Point mousePoint = mouseEvent.getPoint();
        clickedCellIndex = getIndexInGrid(mousePoint);
        Integer clickedElementKey = getClickedElementKey(mousePoint);
        if (clickedElementKey != null)
        {
            model.setSelectedItem(clickedElementKey);
        }
        else if (!preventMouseEventHandler)
        {
            Point index = getIndexInGrid(mousePoint);
            addNodeToModel(index);
        }
        preventMouseEventHandler = false;
    }

    private boolean areCellNeighboursEmpty(Point cellIndex)
    {
        int i = cellIndex.y;
        int j = cellIndex.x;
        return isCellEmpty(i - 1, j) && isCellEmpty(i + 1, j) &&
               isCellEmpty(i, j - 1) && isCellEmpty(i, j + 1) &&
               isCellEmpty(i - 1, j - 1) && isCellEmpty(i - 1, j + 1) &&
               isCellEmpty(i + 1, j - 1) && isCellEmpty(i + 1, j + 1);
    }

    private boolean isCellEmpty(int i, int j)
    {
        if ((i < 0) || (i >= ROWS_COUNT) ||
            (j < 0) || (j >= COLS_COUNT))
        {
            return false;
        }
        return (nodeLabelMatrix[i][j] == null);
    }

    private void handlePopupTrigger(MouseEvent mouseEvent)
    {
        Point clickedPoint = mouseEvent.getPoint();
        Integer clickedElementKey = getClickedElementKey(clickedPoint);
        GraphPanelPopupType graphPanelPopupType = GraphPanelPopupType.EMPTY_POPUP;
        if (clickedElementKey != null)
        {
            NetworkElement clickedElement = model.getElement(clickedElementKey);
            if (clickedElement instanceof Node)
            {
                graphPanelPopupType = GraphPanelPopupType.NODE_POPUP;
            }
            else if (clickedElement instanceof Edge)
            {
                graphPanelPopupType = GraphPanelPopupType.EDGE_POPUP;
            }
        }
        else
        {
            Point index = getIndexInGrid(clickedPoint);
            if (!areCellNeighboursEmpty(index))
            {
                return;
            }
        }
        showPopup(mouseEvent, graphPanelPopupType);
    }

    private NodeLabel findNodeLabelByPoint(Point mousePoint)
    {
        Point index = getIndexInGrid(mousePoint);
        return nodeLabelMatrix[index.y][index.x];
    }

    private Integer getClickedElementKey(Point mousePoint)
    {
        NodeLabel nodeLabel = findNodeLabelByPoint(mousePoint);
        return (nodeLabel != null) ? nodeLabel.getKey() : findSelectedEdgeKey(mousePoint);
    }

    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        drawGrid(g);
        drawEdges(g);
    }

    private Integer findSelectedEdgeKey(Point mousePoint)
    {
        Integer selectedEdgeKey = null;
        double minDistance = EDGE_CLICK_DISTANCE;
        for (Map.Entry<Integer, Edge> edgeEntry : model.getEdges().entrySet())
        {
            Edge edge = edgeEntry.getValue();
            Point startPoint = getNodeCenter(edge.getNode1Key());
            Point endPoint = getNodeCenter(edge.getNode2Key());
            if (edge.isReservedLine())
            {
                startPoint.translate(RESERVED_LINE_OFFSET, RESERVED_LINE_OFFSET);
                endPoint.translate(RESERVED_LINE_OFFSET, RESERVED_LINE_OFFSET);
            }
            double distance = getMinimumDistance(startPoint, endPoint, mousePoint);
            if (distance < minDistance)
            {
                minDistance = distance;
                selectedEdgeKey = edgeEntry.getKey();
            }
        }
        return selectedEdgeKey;
    }

    private double getMinimumDistance(Point v, Point w, Point p)
    {
        double l2 = v.distanceSq(w);
        if (l2 == 0) return p.distance(v); //linePoint1 == linePoint2
        // Consider the line extending the segment, parameterized as v + t (w - v).
        // We find projection of point p onto the line.
        // It falls where t = [(p-v) . (w-v)] / |w-v|^2
        double t = ((p.getX() - v.getX()) * (w.getX() - v.getX()) + (p.getY() - v.getY()) * (w.getY() - v.getY())) / l2;
        if (t < 0) return p.distance(v);
        if (t > 1) return p.distance(w);
        Point projection = new Point();
        projection.setLocation(
            v.x + t * (w.x - v.x),
            v.y + t * (w.y - v.y)
        );

        return p.distance(projection);
    }

    private void drawEdges(Graphics g)
    {
        NetworkElement selectedItem = model.getSelectedItem();
        for (Map.Entry<Integer, Edge> entry : model.getEdges().entrySet())
        {
            Edge edge = entry.getValue();
            Integer key = entry.getKey();
            if (edge.equals(selectedItem))
            {
                g.setColor(new Color(0, 196, 79));
                ((Graphics2D) g).setStroke(new BasicStroke(3));
                if (edge.isReservedLine())
                {
                    drawReservedLine(g, key, edge);
                }
                else
                {
                    drawEdge(g, key, edge);
                }
            }
            else if (edge.isReservedLine())
            {
                g.setColor(new Color(0, 0, 178));
                ((Graphics2D) g).setStroke(new BasicStroke(2));
                drawReservedLine(g, key, edge);
            }
            else
            {
                g.setColor(new Color(0, 130, 240));
                ((Graphics2D) g).setStroke(new BasicStroke(2));
                drawEdge(g, key, edge);
            }
        }
    }

    private Point getNodeCenter(Integer nodeKey)
    {
        Node node = model.getNodes().get(nodeKey);
        int column = node.getColumn();
        int row = node.getRow();
        Point position = getPositionInGrid(row, column);
        int halfCell = CELL_SIZE / 2;
        position.translate(halfCell, halfCell);
        return position;
    }

    private void drawReservedLine(Graphics g, Integer key, Edge edge)
    {
        Point startPoint = getNodeCenter(edge.getNode1Key());
        Point endPoint = getNodeCenter(edge.getNode2Key());
        startPoint.translate(RESERVED_LINE_OFFSET, RESERVED_LINE_OFFSET);
        endPoint.translate(RESERVED_LINE_OFFSET, RESERVED_LINE_OFFSET);
        g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
        int middlePointX = (endPoint.x + startPoint.x) / 2;
        int middlePointY = (endPoint.y + startPoint.y) / 2;
        g.setColor(new Color(0, 0, 0));
        g.setFont(new Font("TimesRoman", Font.BOLD, 12));
        g.drawString(key.toString(), middlePointX + 3, middlePointY + 15);
    }

    private void drawEdge(Graphics g, Integer key, Edge edge)
    {
        Point startPoint = getNodeCenter(edge.getNode1Key());
        Point endPoint = getNodeCenter(edge.getNode2Key());
        g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
        int middlePointX = (endPoint.x + startPoint.x) / 2;
        int middlePointY = (endPoint.y + startPoint.y) / 2;
        g.setColor(new Color(0, 0, 0));
        g.setFont(new Font("TimesRoman", Font.BOLD, 12));
        g.drawString(key.toString(), middlePointX - EDGE_LABEL_OFFSET_X, middlePointY - EDGE_LABEL_OFFSET_Y);
    }

    private void addNodeLabel(int row, int column, Integer key)
    {
        Point point = getPositionInGrid(row, column);
        int x = (int) point.getX();
        int y = (int) point.getY();
        NodeLabel nodeLabel = new NodeLabel(key);
        nodeLabel.setBounds(x + 1, y + 1, NodeLabel.SIZE, NodeLabel.SIZE);
        add(nodeLabel, null);
        nodeLabelMatrix[row][column] = nodeLabel;
    }

    private void updateItems()
    {
        nodeLabelMatrix = new NodeLabel[ROWS_COUNT][COLS_COUNT];
        NetworkElement selectedItem = model.getSelectedItem();
        Nodes nodes = model.getNodes();
        for (Map.Entry<Integer, Node> nodeEntry : nodes.entrySet())
        {
            Node node = nodeEntry.getValue();
            addNodeLabel(node.getRow(), node.getColumn(), nodeEntry.getKey());
            if (node.equals(selectedItem))
            {
                NodeLabel labelToSelect = nodeLabelMatrix[node.getRow()][node.getColumn()];
                labelToSelect.setSelected(true);
            }
        }
    }

    private Point getPositionInGrid(int row, int column)
    {
        int x = column * CELL_SIZE + EXTERNAL_BORDER_OFFSET;
        int y = row * CELL_SIZE + EXTERNAL_BORDER_OFFSET;
        return new Point(x, y);
    }

    private Point getIndexInGrid(Point point)
    {
        return new Point(
            (point.x - EXTERNAL_BORDER_OFFSET) / CELL_SIZE,
            (point.y - EXTERNAL_BORDER_OFFSET) / CELL_SIZE
        );
    }

    private void drawGrid(Graphics g)
    {
        int colsCount = getWidth() / (CELL_SIZE - GRID_SIZE);
        int rowsCount = getHeight() / (CELL_SIZE - GRID_SIZE);
        g.setColor(new Color(105, 105, 105));
        for (int i = 1; i <= colsCount; i++)
        {
            int cellOffset = CELL_SIZE * i + EXTERNAL_BORDER_OFFSET;
            g.drawLine(cellOffset, 0, cellOffset, getHeight());
        }
        for (int i = 1; i <= rowsCount; i++)
        {
            int cellOffset = CELL_SIZE * i + EXTERNAL_BORDER_OFFSET;
            g.drawLine(0, cellOffset, getWidth(), cellOffset);
        }
    }

    public void popupMenuWillBecomeVisible(PopupMenuEvent e)
    {
        preventMouseEventHandler = true;
    }

    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { }

    public void popupMenuCanceled(PopupMenuEvent e) { }

    public void mouseReleased(MouseEvent mouseEvent)
    {
        if (mouseEvent.isPopupTrigger())
        {
            handlePopupTrigger(mouseEvent);
        }
        else if (mouseEvent.getButton() == MouseEvent.BUTTON1)
        {
            handleLeftMouseReleasedEvent(mouseEvent);
        }
    }

    private void handleLeftMouseReleasedEvent(MouseEvent mouseEvent)
    {
        if (!mouseIsInside)
            return;

        NetworkElement selectedItem = model.getSelectedItem();
        if (selectedItem instanceof Node)
        {
            Point mousePoint = mouseEvent.getPoint();
            Point index = getIndexInGrid(mousePoint);
            if (!index.equals(clickedCellIndex))
            {
                Point selectedItemIndex = new Point(
                        ((Node) selectedItem).getColumn(),
                        ((Node) selectedItem).getRow()
                );
                if (selectedItemIndex.equals(clickedCellIndex))
                {
                    NodeLabel nodeLabel = findNodeLabelByPoint(mousePoint);
                    if (nodeLabel != null)
                    {
                        model.addEdgeFromSelectedNode(nodeLabel.getKey());
                    }
                    else
                    {
                        Integer newNodeKey = addNodeToModel(index);
                        if (newNodeKey != null)
                        {
                            model.addEdgeFromSelectedNode(newNodeKey);
                        }
                    }
                }
            }
        }
    }

    private Integer addNodeToModel(Point nodeIndex)
    {
        Integer newNodeKey = null;
        if (areCellNeighboursEmpty(nodeIndex))
        {
            newNodeKey = model.addNode(nodeIndex.y, nodeIndex.x);
        }
        return newNodeKey;
    }

    private void showPopup(MouseEvent mouseEvent, GraphPanelPopupType popupType)
    {
        lastPopupTriggeredPoint = mouseEvent.getPoint();
        popup.updateContext(popupType);
        popup.show(this, mouseEvent.getX(), mouseEvent.getY());
    }

    public void mouseClicked(MouseEvent mouseEvent) {}

    public void mouseEntered(MouseEvent e)
    {
        mouseIsInside = true;
    }

    public void mouseExited(MouseEvent e)
    {
        mouseIsInside = false;
    }
}
